require 'cstruct'

class Header_Block < CStruct
  char :hdr, [4]  # HDR*
  char :name, [14]  # 'PowerBuilder' + 0x00 + 0x00
  char :version, [4]  # PBL format version? (0400/0500/0600)
  int32 :modified_date  # Creation/Modification datetime
  char :reserved0, [2]
  char :comment, [256]  # Library comment
  int32 :first_scc_data_offset  # Offset of first SCC data block
  int32 :scc_data_size # Net size of SCC data
  char :reserved1, [220]
end

class Bitmap_Block < CStruct
  char :fre, [4]  # FRE*
  int32 :next_block_offset  # Offset of next block or 0
  char :bitmap, [504]  # Each bit represents a block, 4032 blocks are referenced
end

class Node_Block < CStruct
  char :nod, [4]  # NOD*
  int32 :left_block_offset  # Offset of next (left) block or 0
  int32 :parent_block_offset  # Offset of parent block or 0
  int32 :right_block_offset  # Offset of next (right) block or 0
  int16 :space_left_in_block  # Space left in block, initial = 3040
  int16 :first_objectname_position  # Position of alphabetically last object name in this block
  int16 :entries_count  # Count of entries in that node
  int16 :last_objectname_position  # Position of alphabetically first object name in this block
  char :reserved0, [8]
  char :entry_chunks, [3040]  # 'ENT*'-Chunks
end

class Entry_Chunk < CStruct
  char :ent, [4]  # ENT*
  char :version, [4]  # PBL format version? (0400/0500/0600)
  int32 :first_data_block_offset
  int32 :object_size  # Net size of object
  int32 :modified_date  # Creation/Modification datetime
  int16 :comment_length  # Length of comment
  int16 :object_name_length  # Length of object name
end

class Data_Block < CStruct
  char :dat, [4]  # 'DAT*'
  int32 :next_data_block_offset
  int16 :block_data_length
  char :blob, [502]
end

def int_array_to_str(array)
  array.pack("C#{array.size}")
end

def int_to_hex_str(value)
  '0X' + value.to_s(16).upcase.rjust(8, '0')
end

def read_header_block(sdata, offset)
  header = Header_Block.new
  header << sdata.slice(offset, Header_Block.size)

  print int_array_to_str(header.hdr) + "\t"
  print int_array_to_str(header.name).rstrip + "\t"
  print int_array_to_str(header.version) + "\t"
  print Time.at(header.modified_date).to_s + "\n"
end

def read_bitmap_block(sdata, offset)
  bitmap = Bitmap_Block.new
  bitmap << sdata.slice(offset, Bitmap_Block.size)

  print int_array_to_str(bitmap.fre) + "\t"
  print "Next Offset: " + int_to_hex_str(bitmap.next_block_offset) + "\n"

  return bitmap.next_block_offset
end

def read_bitmap_blocks(sdata, offset)
  next_block_offset = offset
  begin
    next_block_offset = read_bitmap_block(sdata, next_block_offset)
  end while next_block_offset != 0
end

def read_node_block(sdata, offset)
  node = Node_Block.new
  node << sdata.slice(offset, Node_Block.size)

  print int_array_to_str(node.nod) + "\t"
  print "Left Offset: " + int_to_hex_str(node.left_block_offset) + "\t"
  print "Parent Offset: " + int_to_hex_str(node.parent_block_offset) + "\t"
  print "Right Offset: " + int_to_hex_str(node.right_block_offset) + "\t"
  print "Entries' Count: " + int_to_hex_str(node.entries_count) + "\n"

  return node
end

def read_object_data(sdata, offset, length)
  object_data = ''
  data_block_offset = offset
  while data_block_offset != 0
    data_block_offset, data_blob = read_data_block(sdata, data_block_offset)
    object_data << data_blob
  end
  object_data.slice!(0, length)  # Remove header comment

  return object_data.rstrip
end

def read_node_blocks(sdata, offset)
  node = read_node_block(sdata, offset)

  object_list = read_entry_chunks(int_array_to_str(node.entry_chunks), node.entries_count)

  object_list.each do |object|
    object_name = object[:name]

    next unless object_name =~ /\.sr[a-z]\z/i

    puts "\n#### BEGIN " + object_name
    puts read_object_data(sdata, object[:offset], object[:length])
    puts "#### END " + object_name + "\n"
  end

  read_node_blocks(sdata, node.left_block_offset) if node.left_block_offset != 0
  read_node_blocks(sdata, node.right_block_offset) if node.right_block_offset != 0
end

def read_entry_chunk(chunks, offset, i)
  entry = Entry_Chunk.new
  entry << chunks.slice(offset, Entry_Chunk.size)

  print "\t\t"
  print int_array_to_str(entry.ent) + i.to_s + "\t"
  print int_array_to_str(entry.version) + "\t"
  print Time.at(entry.modified_date).to_s + "\t"
  print "Data Offset: " + int_to_hex_str(entry.first_data_block_offset) + "\t"
  print "Comment Length: " + int_to_hex_str(entry.comment_length) + "\t"
  print "Net Size: " + int_to_hex_str(entry.object_size) + "\t"

  return entry
end

def read_object_name(chunks, offset, object_name_length)
  offset += Entry_Chunk.size  # Calculate the offset of the object name
  object_name = chunks.slice(offset, object_name_length).rstrip
  offset += object_name_length

  return object_name, offset
end

def read_entry_chunks(chunks, count)
  # [{name: 'example.sra', offset: 0x00001000}, ..., {name: 'example.pra', offset: 0x00001400}]
  object_list = []

  offset = 0
  count.times do |i|
    entry = read_entry_chunk(chunks, offset, i)
    object_name_length = entry.object_name_length
    first_data_block_offset = entry.first_data_block_offset
    comment_length = entry.comment_length

    object_name, offset = read_object_name(chunks, offset, object_name_length)
    print "Name: " + object_name + "\n"

    object_list << {
        name: object_name,
        offset: first_data_block_offset,
        length: comment_length
    }
  end

  return object_list
end

def read_data_block(sdata, offset)
  data = Data_Block.new
  data << sdata.slice(offset, Data_Block.size)

  data_blob = int_array_to_str(data.blob)

  return data.next_data_block_offset, data_blob
end

pbl_file = ARGV[0]
pbl_file_path = File.join(Dir.pwd, pbl_file)

sdata = File.open(pbl_file_path, 'rb').readlines.join

offset = 0
read_header_block(sdata, 0)

offset += Header_Block.size
read_bitmap_blocks(sdata, offset)

offset += Bitmap_Block.size
read_node_blocks(sdata, offset)
